<?php

/**
* Class này cho phép hệ thống xác thực của Yii ứng xử với HttpAuth
*
* @property string $username
* @property HttpUser $currentUser signed in user, FALSE nếu không có người login
* @property string $realm
*/
class HttpUserComponent extends CApplicationComponent implements IWebUser
{
	public $autoResponse401OnUnauthorized=false;

	private $_currentUser;

	private $_username;

	function getUsername()
	{
		if (!isset($this->_username)) $this->authorize();

		return $this->_username;
	}

	/**
	* Get current user
	*
	* @return HttpUser FALSE nếu current User không hợp lệ
	*/
	function getCurrentUser()
	{
		if (!isset($this->_currentUser)) $this->authorize();
		return $this->_currentUser;
	}

	/**
	 * Returns a value that uniquely represents the identity.
	 * @return mixed a value that uniquely represents the identity (e.g. primary key value).
	 */
	public function getId()
	{
		return $this->getUsername();
	}
	/**
	 * Returns the display name for the identity (e.g. username).
	 * @return string the display name for the identity.
	 */
	public function getName()
	{
		return $this->getUsername();
	}

	/**
	 * Returns a value indicating whether the user is a guest (not authenticated).
	 * @return boolean whether the user is a guest (not authenticated)
	 */
	public function getIsGuest()
	{
		$x = $this->getUsername();
		return empty($x);
	}

	/**
	 * Performs access check for this user.
	 * @param string $operation the name of the operation that need access check.
	 * @param array $params name-value pairs that would be passed to business rules associated
	 * with the tasks and roles assigned to the user.
	 * @return boolean whether the operations can be performed by this user.
	 */
	public function checkAccess($operation,$params=array())
	{
		return Yii::app()->getAuthManager()->checkAccess($operation,$this->getId(),$params);
	}

	private $_realm;

	function getRealm()
	{
		if (!isset($this->_realm)) $this->_realm = md5(Yii::app()->request->requestUri);

		return $this->_realm;
	}

	function setRealm($v)
	{
		$this->_realm = $v;
	}

	public function loginRequired()
	{
		$realm = $this->getRealm();
		$nonce = rand(100, 999).time();

		header('HTTP/1.1 401 Unauthorized');
		header('WWW-Authenticate: Digest '.
			'realm="'.$realm.'"'.
			',qop="auth",nonce="'.$nonce.'",opaque="'.sprintf('%x', crc32($realm)).'"');

		header('Content-Type: text/html;charset=utf8');

		if (isset($this->view401)) Yii::app()->controller->render($this->view401);
		else die('<center><h1>'.Yii::t('message', 'Restricted Area').'</h1></center>');

		Yii::app()->end();

		return false;
	}

	protected function responseForBidden()
	{
		$realm = $this->getRealm();
		$nonce = rand(100, 999).time();

		header('HTTP/1.1 403 ForBidden');

		if (isset($this->view403)) Yii::app()->controller->render($this->view403);
		else die('<center><h1>'.Yii::t('message', 'You came from a disallowed IP').'</h1></center>');

		Yii::app()->end();

		return false;
	}

	function onUnauthorized()
	{
		if ($this->autoResponse401OnUnauthorized) $this->loginRequired();

		return false;
	}

	/**
	* @param string $username User name
	* @return HttpUser
	*/
	protected function getUser($username)
	{
		Yii::import('ext.common.HttpAuth.models.HttpUser');

		$x = HttpUser::model()->findByAttributes(array('username'=>$username));

		return $x;
	}

	protected function parseDigestHeader($txt)
	{
	    // protect against missing data
	    $parts = array('realm','nonce','nc', 'cnonce', 'qop','username','uri','response','opaque');
	    $data = array();
	    $keys = implode('|', $parts);

	    preg_match_all('@('.$keys.')=(?:([\'"])([^\2]+?)\2|([^\s,]+))@', $txt, $matches, PREG_SET_ORDER);

	    foreach ($matches as $m) {
	        $data[$m[1]] = $m[3] ? $m[3] : $m[4];
	    }

	    if (count($parts) != count($data)) return false;

	    if (sprintf('%x', crc32($data['realm'])) != $data['opaque']) return false;

	    $username = $data['username'];

	    $httpUser = $this->getUser($username);
	    if ($httpUser === false) return false;

	    if (is_null($httpUser)) return false;
	    $password = $httpUser->password;

	    $h1 = md5($username . ':' . $data['realm'] . ':' . $password);
		$h2 = md5($_SERVER['REQUEST_METHOD'].':'.$data['uri']);
		$validResponse = md5($h1.':'.$data['nonce'].':'.$data['nc'].':'.$data['cnonce'].':'.$data['qop'].':'.$h2);

		if ($validResponse != $data['response']) return false;

	    return $httpUser;
	}

	/**
	* Is an IP matches a rule?
	*
	* @param string $ip ip
	* @param string $rule rule in wildcard formation
	*
	* @return boolean
	*/
	protected function ipMatchesRule($ip, $rule)
	{
		$rule = trim($rule, " \r\n\t");

		$rule = preg_quote($rule, '.:/');

		$rule = str_replace('\*', '.*', $rule);
		$rule = str_replace('\?', '.', $rule);

		$rule = '|^'.$rule.'$|s';

		return preg_match($rule, $ip);
	}

	protected function verifyIP()
	{
		$clientIp = isset($_SERVER["HTTP_X_FORWARDED_FOR"]) ? $_SERVER["HTTP_X_FORWARDED_FOR"] :
						(
							isset($_SERVER["REMOTE_ADDR"]) ? $_SERVER["REMOTE_ADDR"] :
							(
								isset($_SERVER["HTTP_CLIENT_IP"]) ? $_SERVER["HTTP_CLIENT_IP"] : '127.0.0.1'
							)
						);

		$httpUser = $this->currentUser;
		if (!$httpUser) return $this->onUnauthorized();

		$whiteIps = explode("\n", $httpUser->white_ips);
		$blackIps = explode("\n", $httpUser->black_ips);

		$blocked = false;
		foreach ($blackIps as $rule) {
			if ($blocked) break;
			if ($this->ipMatchesRule($clientIp, $rule)) $blocked = true;
		}

		if ($blocked)
			foreach ($whiteIps as $rule)
				if ($this->ipMatchesRule($clientIp, $rule)) {
					$blocked = false;
					break;
				}

		if ($blocked) return $this->responseForBidden();
	}

	/**
	* Xác thực người dùng
	*
	*/
	function authorize()
	{
		$realm = $this->getRealm();

		# nếu chưa có thông tin xác thực gửi lên
		if (!isset($_SERVER['PHP_AUTH_DIGEST']) || empty($_SERVER['PHP_AUTH_DIGEST']))
			return $this->onUnauthorized();

		if (!($this->_currentUser = $this->parseDigestHeader($_SERVER['PHP_AUTH_DIGEST'])))
			return $this->onUnauthorized();

		$this->_username = $this->_currentUser->username;

		$this->verifyIP();

		return true;
	}
	
	/**
	* Function kiểm tra Signature có hợp lệ hay không!
	* 
	* 1. Thêm Field rsa_public vào table http_user
	* 2. Check signature bằng expression accessRules trong controller tương ứng
	*/
	function verifySignature()
	{
		Yii::import('ext.helpers.RestHelper');
		$signature = Yii::app()->request->getQuery('signature', null);
		if (empty($signature)) throw new CHttpException(400, 'Signature is missing in GET');
		
		if(!isset($this->_currentUser->rsa_public) || empty($this->_currentUser->rsa_public))
			throw new CHttpException(400, 'RSA Public key is missing');
		
		$get = isset($_GET) ? $_GET : null;
		$post = isset($_POST) ? $_POST : null;
		
		unset($get['signature']);
		
		$isValid = RestHelper::verifySignature($this->_currentUser->rsa_public, $signature, null, null, $get, $post);
		if (!$isValid) throw new CHttpException(406, 'Invalid signature');
		
		return true;
	}
}